package site.sishidadao.morphtext;

import android.animation.Animator;
import android.animation.AnimatorSet;
import android.content.Context;
import android.graphics.Color;
import android.graphics.Typeface;
import android.util.AttributeSet;
import android.view.animation.Interpolator;
import android.widget.AbsoluteLayout;
import android.widget.TextView;

import androidx.annotation.Nullable;

import java.util.ArrayList;
import java.util.List;

public class MorphTextview extends AbsoluteLayout {
    public enum MorphEffect {
        None,       // 默认效果
        Scale,      // 缩放
        Evaporate,  // 蒸发
        Fall,       // 掉落
        RollingFromLeft,    // 滚蛋动画效果
        RollingFromRight,
    }

    public enum HorizontalAlignment {
        Left, Center, Right
    }

    private Context mContext;
    private MorphTextview mSelf;

    private List<String> mMorphStrings = new ArrayList<>( );
    private boolean mIsMorphing = false;

    private String mCurrentStr = "";
    private List<MorphChar> mCurrentMorphChars = new ArrayList<>( );

    private boolean mEnableStayAnimation = false;
    private boolean mIsStayAnimating = false;

    public MorphEffect MEffect = MorphEffect.Evaporate;
    public HorizontalAlignment Alignment = HorizontalAlignment.Center;
    public float FontSize = 20;
    public int FontColor = Color.parseColor( "#CC9966" );
    public Typeface FontTypeface = Typeface.DEFAULT_BOLD;

    public MorphTextview( Context context ) {
        this( context, null, 0 );
    }

    /**
     * 在xml布局文件中使用时自动调用
     */
    public MorphTextview( Context context, @Nullable AttributeSet attrs ) {
        this( context, attrs, 0 );
    }

    /**
     * 不会自动调用，如果有默认style时，在第二个构造函数中调用
     */
    public MorphTextview( Context context, @Nullable AttributeSet attrs, int defStyleAttr ) {
        super( context, attrs, defStyleAttr );

        this.init( context );
    }

    private void init( Context context ) {
        mContext = context;
        mSelf = this;

        inflate( context, R.layout.widget_morph_textview, this );
    }

    public void morph( String str ) {
        this.mMorphStrings.add( str );
        this.peekNextAnimation( );
    }

    List<MorphChar> buildSubByChar( List<MorphChar> from, char matchChar ) {
        List<MorphChar> sub = new ArrayList<>( );

        for ( MorphChar morphChar : from )
            if ( morphChar.Char == matchChar )
                sub.add( morphChar );

        return sub;
    }

    int idxOfChar( List<MorphChar> checked, char checkedChar ) {
        for ( int i = 0; i < checked.size( ); i++ )
            if ( checked.get( i ).Char == checkedChar )
                return i;
        return -1;
    }

    List<MorphChar> buildFinalChars( String from, String to ) {
        List<MorphChar> finalChars = new ArrayList<>( );

        for ( int i = 0; i < from.length( ); i++ ) {
            char charChecked = from.charAt( i );

            MorphChar morphChar = this.mCurrentMorphChars.remove(
                    this.idxOfChar( this.mCurrentMorphChars, charChecked ) );
            morphChar.FromIdx = i;
            morphChar.ToIdx = -1;

            finalChars.add( morphChar );
        }
        this.mCurrentMorphChars.addAll( finalChars );

        List<MorphChar> toChars = new ArrayList<>( );
        for ( int i = 0; i < to.length( ); i++ ) {
            MorphChar morphChar = new MorphChar( );
            morphChar.Char = to.charAt( i );
            morphChar.ToIdx = i;
            toChars.add( morphChar );
        }

        // scan from again to determine ToIdx in each fromChar
        List<Integer> scannedFromIdxes = new ArrayList<>( );
        List<Integer> scannedToIdxes = new ArrayList<>( );
        for ( int i = 0; i < from.length( ); i++ ) {
            MorphChar fromChar = this.mCurrentMorphChars.get( i );
            if ( scannedFromIdxes.contains( fromChar.FromIdx ) ) continue;

            char targetChar = fromChar.Char;
            List<MorphChar> fromMatches = this.buildSubByChar( this.mCurrentMorphChars, targetChar );
            List<MorphChar> toMatches = this.buildSubByChar( toChars, targetChar );

            for ( int j = 0; j < fromMatches.size( ); j++ ) {
                MorphChar fromCharCheck = fromMatches.get( j );
                if ( j < toMatches.size( ) )
                    fromCharCheck.ToIdx = toMatches.get( j ).ToIdx;
                scannedFromIdxes.add( fromCharCheck.FromIdx );
            }

            if ( fromMatches.size( ) < toMatches.size( ) )
                for ( int j = fromMatches.size( ); j < toMatches.size( ); j++ ) {
                    MorphChar toCharCheck = toMatches.get( j );

                    TextView tvChar = new TextView( mContext );
                    tvChar.setText( String.valueOf( toCharCheck.Char ) );
//                    tvChar.setTextColor( mContext.getResources( ).getColor( R.color.colorTransparent ) );
//                    tvChar.measure( 0, 0 );
                    toCharCheck.TextViewRef = tvChar;

                    finalChars.add( toCharCheck );
                    scannedToIdxes.add( toCharCheck.ToIdx );
                }

            scannedFromIdxes.add( fromChar.FromIdx );
        }

        // scan to again to determine which one shall be associated with a textview, then add it to final chars
        for ( int i = 0; i < to.length( ); i++ ) {
            MorphChar toChar = toChars.get( i );
            if ( scannedToIdxes.contains( toChar.ToIdx ) ) continue;

            char targetChar = toChar.Char;
            List<MorphChar> fromMatches = this.buildSubByChar( this.mCurrentMorphChars, targetChar );
            if ( fromMatches.size( ) == 0 ) {
                TextView tvChar = new TextView( mContext );
                tvChar.setText( String.valueOf( targetChar ) );
//                tvChar.setTextColor( mContext.getResources( ).getColor( R.color.colorTransparent ) );
//                tvChar.measure( 0, 0 );
                toChar.TextViewRef = tvChar;

                finalChars.add( toChar );
            }

            scannedToIdxes.add( toChar.ToIdx );
        }

        return finalChars;
    }

    private void morph( String from, String to ) {
        mIsMorphing = true;

        CharMorphLimbo.ExtraInfo extraInfo = new CharMorphLimbo.ExtraInfo( );
        extraInfo.WidgetWidth = this.getMeasuredWidth( );
        extraInfo.HorizontalAlignment = this.Alignment;

        List<MorphChar> finalChars = this.buildFinalChars( from, to );
        extraInfo.FinalChars = finalChars;

        int entersCount = 0;
        int shiftsCount = 0;
        int leavesCount = 0;
        for ( MorphChar morphChar : finalChars ) {
            this.setTextviewStyle( morphChar.TextViewRef );

            if ( morphChar.FromIdx != -1 )
                extraInfo.FromTotalWidth += morphChar.TextViewRef.getMeasuredWidth( );
            if ( morphChar.ToIdx != -1 )
                extraInfo.ToTotalWidth += morphChar.TextViewRef.getMeasuredWidth( );
            switch ( morphChar.getLimboType( ) ) {
                case Enter:
                    entersCount += 1;
                    break;
                case Shift:
                    shiftsCount += 1;
                    break;
                case Leave:
                    leavesCount += 1;
                    break;
            }
        }
        extraInfo.YPos = ( this.getMeasuredHeight( ) - finalChars.get( 0 ).TextViewRef.getMeasuredHeight( ) ) / 2;
        long[] entersDelays = new long[ entersCount ];
        long[] leavesDelays = new long[ leavesCount ];

        switch ( this.MEffect ) {
            case None:
                break;
            case Scale:
                CharLimbo.RandomInterpolator interpolatorScale = new CharLimbo.RandomInterpolator( );
                this.buildDelayWithInterpolator( 300, entersDelays, interpolatorScale );
                this.buildDelayWithInterpolator( 300, leavesDelays, interpolatorScale );
                break;
            case Evaporate:
                CharLimbo.RandomInterpolator interpolatorEvaporate = new CharLimbo.RandomInterpolator( );
                this.buildDelayWithInterpolator( 300, entersDelays, interpolatorEvaporate );
                this.buildDelayWithInterpolator( 300, leavesDelays, interpolatorEvaporate );
                break;
        }

        int enterIdx = 0;
        int shiftIdx = 0;
        int leaveIdx = 0;
        List<Animator> animators = new ArrayList<>( );
        for ( MorphChar morphChar : finalChars ) {
            CharMorphLimbo limbo = null;

            switch ( this.MEffect ) {
                case None:
                    limbo = new CharMorphLimbo( morphChar, 0, 500, extraInfo );
                    break;
                case Scale:
                    limbo = new CharMorphLimboScale( morphChar, 0, 500, extraInfo );
                    break;
                case Evaporate:
                    limbo = new CharMorphLimboEvaporate( morphChar, 0, 500, extraInfo );
                    break;
            }

            if ( morphChar.getLimboType( ) == CharLimbo.LimboType.Enter ) {
                morphChar.TextViewRef.setAlpha( 0 );
                this.addView( morphChar.TextViewRef, limbo.getStartupLayoutParas( ) );
            }

            switch ( morphChar.getLimboType( ) ) {
                case Enter:
                    limbo.Delay = entersDelays[ enterIdx ];
                    enterIdx += 1;
                    break;
                case Shift:
                    shiftIdx += 1;
                    break;
                case Leave:
                    limbo.Delay = leavesDelays[ leaveIdx ];
                    leaveIdx += 1;
                    break;
            }

            limbo.attachAnimations( animators );
        }

        AnimatorSet animatorSet = new AnimatorSet( );
        animatorSet.addListener( new Animator.AnimatorListener( ) {
            @Override
            public void onAnimationStart( Animator animator ) {
            }

            @Override
            public void onAnimationEnd( Animator animator ) {
                for ( int i = finalChars.size( ) - 1; i >= 0; i-- )
                    if ( finalChars.get( i ).getLimboType( ) == CharLimbo.LimboType.Leave ) {
                        mSelf.removeView( finalChars.get( i ).TextViewRef );
                        finalChars.remove( i );
                    }
                mCurrentStr = to;
                mCurrentMorphChars = finalChars;
                mMorphStrings.remove( 0 );

                mIsMorphing = false;
                peekNextAnimation( );
            }

            @Override
            public void onAnimationCancel( Animator animator ) {
            }

            @Override
            public void onAnimationRepeat( Animator animator ) {
            }
        } );

        animatorSet.playTogether( animators );
        animatorSet.start( );
    }

    private void peekNextAnimation( ) {
        if ( !this.mIsMorphing && !mIsStayAnimating && this.mMorphStrings.size( ) != 0 ) {
            if ( this.mCurrentStr.equals( this.mMorphStrings.get( 0 ) ) )
                this.mMorphStrings.remove( 0 );
            else
                this.morph( this.mCurrentStr, this.mMorphStrings.get( 0 ) );
        } else if ( !this.mIsMorphing && !mIsStayAnimating && this.mEnableStayAnimation )
            this.startStayAnimation( );
    }

    private void startStayAnimation( ) {

    }

    void buildDelayWithInterpolator( long maxDelay, long[] delays, Interpolator interpolator ) {
        float totalLength = delays.length;
        for ( int i = 0; i < delays.length; i++ ) {
            float delay = interpolator.getInterpolation( ( i + 1 ) / totalLength ) * maxDelay;
            delays[ i ] = ( long ) delay;
        }
    }

    void setTextviewStyle( TextView tv ) {
        tv.setTextColor( FontColor );
        tv.setTypeface( FontTypeface );
        tv.setTextSize( FontSize );
        tv.setShadowLayer( 0.5f, 5, 5, Color.BLACK );

        tv.measure( 0, 0 );
    }

    private void setTextInLayout( String content ) {
        this.removeAllViewsInLayout( );

        int xOffset = 0;
        List<LayoutParams> paramsList = new ArrayList<>( );
        List<TextView> textviewsList = new ArrayList<>( );
        for ( int i = 0; i < content.length( ); i++ ) {
            TextView tvChar = new TextView( mContext );
            tvChar.setText( String.valueOf( content.charAt( i ) ) );

            tvChar.measure( 0, 0 );
            AbsoluteLayout.LayoutParams params = new AbsoluteLayout.LayoutParams(
                    tvChar.getMeasuredWidth( ), tvChar.getMeasuredHeight( ), xOffset, 0 );

            paramsList.add( params );
            textviewsList.add( tvChar );
            xOffset += tvChar.getMeasuredWidth( );
        }

        // xOffset is total width at the moment
        int widthOffset = ( this.getMeasuredWidth( ) - xOffset ) / 2;
        int heightOffset = ( this.getMeasuredHeight( ) - paramsList.get( 0 ).height ) / 2;
        for ( int i = 0; i < textviewsList.size( ); i++ ) {
            paramsList.get( i ).x = paramsList.get( i ).x + widthOffset;
            paramsList.get( i ).y = paramsList.get( i ).y + heightOffset;
            textviewsList.get( i ).setLayoutParams( paramsList.get( i ) );

            this.addView( textviewsList.get( i ) );

            MorphChar morphChar = new MorphChar( );
            morphChar.TextViewRef = textviewsList.get( i );
            mCurrentMorphChars.add( morphChar );
        }
    }
}
